Skip to content

JavaScriptEventLoop: Append a job after the queue tail without a scan - #28

Closed
krodak wants to merge 1 commit into
mainfrom
kr/job-queue-tail
Closed

krodak wants to merge 1 commit into
mainfrom
kr/job-queue-tail

Conversation

@krodak

@krodak krodak commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator

Overview

insertJobQueue keeps the queue sorted by descending priority and finds the insertion point by walking the list from the head. A new job goes after every queued job of the same or a higher priority, so a job of the most common case (the same priority as the jobs already queued) walks the whole queue. When a burst of N jobs of one priority is queued before the drain starts, the inserts cost O(N²) link steps.

This happens in practice when an async API produces one job per element. For example, a SQLite query through SQLiteNIO's future-based API makes one job per row, so a read of a few hundred thousand rows queues that many jobs before the drain runs them.

This PR keeps a pointer to the tail of the queue. The tail has the lowest priority in the queue, so a job whose priority is not higher than the tail's goes right after the tail in O(1). A job with a higher priority still scans from the head, as before. The order does not change: higher priority first, and FIFO within one priority.

1. Insert. If the queue has a tail and newJob.rawPriority <= tail.rawPriority, link the job after the tail and make it the new tail. Otherwise use the existing scan, and set the tail when the scan appends at the end (which happens only when the queue was empty).

2. Claim. claimNextFromQueue clears the tail when it takes the last job.

headJob, tailJob and nextInQueue() are fileprivate, and these two functions are the only places that change the links, so the tail cannot become stale.

Measurements

A Swift/Wasm app (Khasm) that writes a stored building graph to SQLite again on reopen, release build, Node.js 22, medians:

Workload Before After Change
Warm write, 21k nodes (n=5) 3,160 ms 2,933 ms -7.2%
Warm write, 210k nodes (n=3 before, n=4 after) 32,816 ms 29,921 ms -8.8%
Same app with fewer queued jobs per read, 210k nodes 15,832 ms 15,329 ms -3.2%
Cold write, 21k nodes and 210k nodes no change

In a CPU profile of the 210k-node write before this change, the job queue took about 11% of the time.

Testing

  • New testTaskPriorityOrderWithMixedPriorities (XCTest on Node.js):
    • a burst of low, medium and high tasks runs high first and keeps FIFO order within each priority;
    • tasks started from inside a running task interleave with the queued ones by priority;
    • 600 tasks over six priorities run in priority order, FIFO within each priority.
      The test also passes on main without this change, so the order is the same as before.
  • make unittest

@krodak

krodak commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator Author

replaced with !823 upstream

@krodak krodak closed this Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant